using System;
using System.IO;
using System.Runtime.Serialization;
using System.Threading;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using BoxBinary.AspectCache.Core.Broadcast;
using BoxBinary.AspectCache.Core.CacheManagement;
using BoxBinary.AspectCache.Core.Helpers;
using BoxBinary.AspectCache.Core.Logging;
using BoxBinary.AspectCache.Core.PolicyDefinition;
using BoxBinary.AspectCache.Core.ReplicationSink;
using Path = BoxBinary.AspectCache.Core.Helpers.Path;

namespace BoxBinary.AspectCache.Core
{
    /// <summary>
    /// Acts as a manager for all items entering or coming out of <see cref="System.Web.Caching.Cache"/>.
    /// <para>Note: Internally relies on the basis that all items in the cache via this manager are stored as 
    /// <see cref="PolicyBoundCachePackage"/> objects, such that upon retrieval information can be determined about
    /// the item such as its key or its footprint.</para>
    /// </summary>
    public class CacheManager
    {
        #region Thread locking object

        // This is a thread locker which is used to synchronise access to CacheRemovedShouldNotify
        public static object CacheRemovedDelegateLockObject = new object();

        #endregion

        #region Supplement built-in cache removal reason information

        // This is used by SinkWatcher in order to let OnCacheItemRemoved know that it
        // should not care about an item having just been removed - in order to avoid infinite loops
        // between the ReplicationSink and the internal cache notification system

        public enum CacheRemovalReason
        {
            NormalRemoval,
            RemovedDueToUpdate
        }

        public static CacheRemovalReason CacheRemovedReason = CacheRemovalReason.NormalRemoval;

        #endregion

        #region Insert methods

        //TODO: Finish this (check exact details of reloaded cache item)
        /// <summary>
        /// Used by <see cref="ReloadAllPermanentCaches"/> to avoid reserializing an item
        /// </summary>
        /// <param name="package"></param>
        /// <param name="loadedFromFilePath"></param>
        protected internal static void ReInsertLoadedCacheItem(WebPolicyBoundCachePackage package, string loadedFromFilePath)
        {
            if (package != null && package.AbsoluteExpiration > DateTime.Now)
            {
                CacheDependency persistenceDependency = null;

                if (package.CacheStore == CacheStoreMechanism.PersistenceProvider)
                {
                    // If it's a filesystem based cache item, persist the filepath
                    // in the Item property of the Package since this is what a normal
                    // cache insertion would do (to use less RAM)
                    persistenceDependency = new CacheDependency(loadedFromFilePath);
                    package.Item = loadedFromFilePath;
                }
                InsertIntoBuiltInCache(package, persistenceDependency, false);
            }
        }

        protected internal static void Insert(WebPolicyBoundCachePackage package, bool notifyReplicationSink)
        {
            if (!CacheIsEnabled)
                return;

            if (package != null && package.AbsoluteExpiration > DateTime.Now)
            {
                CacheDependency persistenceDependency = null;

                if (package.CacheStore == CacheStoreMechanism.PersistenceProvider)
                {
                    // This item needs to be cached to disk.
                    // 1: Generate a filepath for it.
                    // 2: Serialize it to disk.
                    // 3: If successful, replace the PolicyBoundCachePackage.Item with the string path.
                    // 4: Release PolicyBoundCachePackage to be cached as normal for later retrieval of the path value.

                    string persistedFileName = string.Empty;
                    if (PersistToFilesystem(package, out persistedFileName))
                    {
                        package.Item = persistedFileName;
                        persistenceDependency = new CacheDependency(persistedFileName);
                    }
                    else
                    {
                        // Save to disk was unsuccessful, so don't confuse the Retrieve
                        // method because it will try to load from disk - mark the PolicyBoundCachePackage
                        // as being saved to the Default store rather than to the PersistenceProvider
                        package.CacheStore = CacheStoreMechanism.Default;
                    }
                }

                InsertIntoBuiltInCache(package, persistenceDependency, notifyReplicationSink);
            }
        }

        private static void InsertIntoBuiltInCache(WebPolicyBoundCachePackage package, CacheDependency persistenceDependency,
                                                   bool notifyReplicationSink)
        {
            if (!CacheIsEnabled)
                return;

            if (package != null && package.AbsoluteExpiration > DateTime.Now)
            {
                CacheItemRemovedCallback cacheItemRemoved = OnCacheItemRemoved;

                // Set the RemovedReason so we can distinguish between cache removals due to an update,
                // or just a normal delete - internally, the ASP.NET cache makes no such distinction
                CacheRemovedReason = CacheRemovalReason.RemovedDueToUpdate;

                // Note here that the full Package is stored as the cache value, not Package.Item, because
                // upon retrieval we can then access the same properties about the policy as when it went in
                // (for instance, OnCacheItemRemoved it's useful to know if the item was cached to disk).
                // Note: We use HttpRuntime.Cache because this is available irrespective of whether there
                // is a current web request in progress, and this method may be called by the SinkWatcher
                HttpRuntime.Cache.Insert(package.Key, package, persistenceDependency,
                                         package.AbsoluteExpiration,
                                         Cache.NoSlidingExpiration, package.Priority, cacheItemRemoved);

                // Now check if the ReplicationSink is enabled, and if so, tell it to broadcast our
                // new cache item. Changes or deletions are then handled by the SinkWatcher
                if (notifyReplicationSink)
                    SinkManager.Broadcast(BroadcastEventReason.ItemCreated, package);

                // Set the RemovedReason back again
                CacheRemovedReason = CacheRemovalReason.NormalRemoval;
            }
        }

        /// <summary>
        /// Inserts an <see cref="ICacheable"/> into HttpContext.Current.Cache, inferring cache store and priority from the
        /// footprint of the object. Footprint is identified by looking up from the <see cref="ICacheable.CachePolicy"/> specified
        /// on the object.
        /// </summary>
        /// <param name="item"></param>
        public static void Insert(ICacheable item)
        {
            if (!CacheIsEnabled)
                return;

            Insert(new WebPolicyBoundCachePackage(item), Configuration.Current.EnableCacheStateReplicationSink);
        }

        /// <summary>
        /// Inserts an object into HttpContext.Current.Cache, inferring cache store and priority from the footprint of the object.
        /// Footprint is identified by looking up from the default cache category.
        /// </summary>
        /// <param name="item"></param>
        /// <param name="cacheKeyGenerationParams"></param>
        public static void Insert(object item, CacheKeyParamsCollection cacheKeyGenerationParams)
        {
            if (!CacheIsEnabled)
                return;

            Insert(new CacheItemWrapper(item, cacheKeyGenerationParams));
        }

        /// <summary>
        /// Inserts an object into HttpContext.Current.Cache, inferring cache store and priority from the footprint of the object.
        /// Footprint is identified by looking up from the cache category supplied in <paramref name="cacheCategory"/>.
        /// </summary>
        /// <param name="item"></param>
        /// <param name="cacheKeyGenerationParams"></param>
        /// <param name="cacheCategory"></param>
        public static void Insert(object item, CacheKeyParamsCollection cacheKeyGenerationParams, string cacheCategory)
        {
            if (!CacheIsEnabled)
                return;

            Insert(new CacheItemWrapper(item, cacheKeyGenerationParams, cacheCategory));
        }

        /// <summary>
        /// Removes an object from the HttpContext.Current.Cache.
        /// </summary>
        /// <param name="objectType">Type of the object</param>
        /// <param name="instanceParams">Parameters used to identify the instance of the object that has been cached</param>
        public static void Remove(Type objectType, CacheKeyParamsCollection instanceParams)
        {
            HttpRuntime.Cache.Remove(CacheKeyBuilder.GetKey(objectType, instanceParams));
        }

        /// <summary>
        /// Serializes an object instance to disk. The storage folder is either <see cref="GetPermanentDiskPersistenceRootedPath"/> if
        /// the <see cref="WebPolicyBoundCachePackage.ShouldSurviveApplicationRestarts"/> property of <paramref name="policyBoundPackageToCache"/> is true, or
        /// <see cref="GetDiskPersistenceRootedPath"/> if not. The filename is the result of calling <see cref="GetSafePathForSerialization"/>
        /// passing in <see cref="WebPolicyBoundCachePackage.Key.GetHashCode"/>
        /// </summary>
        /// <param name="policyBoundPackageToCache"></param>
        /// <param name="fullPathForSerialization"><code>out</code> returns the final serialized path to the caller</param>
        /// <returns></returns>
        internal static bool PersistToFilesystem(WebPolicyBoundCachePackage policyBoundPackageToCache,
                                                 out string fullPathForSerialization)
        {
            // Initialise 'out' parameter
            fullPathForSerialization = "";

            string persistedFileName =
                Path.GenerateSafeFilenameForSerialization(policyBoundPackageToCache.Key.GetHashCode().ToString());

            string storeRootDir = string.Empty;

            if (policyBoundPackageToCache.ShouldSurviveApplicationRestarts)
                storeRootDir = GetPermanentDiskPersistenceRootedPath();
            else
                storeRootDir = GetDiskPersistenceRootedPath();

            IPolicyBoundCachePackage policyBoundCachePackage = policyBoundPackageToCache;

            try
            {
                SerializationHelper.SerializeCacheItemPolicyToFileSystem(storeRootDir, persistedFileName,
                                                                         policyBoundCachePackage, out fullPathForSerialization);
                return true;
            }
            catch (SerializationException serE)
            {
                ErrorHelper.LogError(
                    new Exception(
                        "PolicyBoundCachePackage determined an item should be cached to disk however it contains unserializable members.",
                        serE), true);
            }
            catch (Exception genE)
            {
                ErrorHelper.LogError(
                    new Exception(
                        string.Format(
                            "General error occurred saving WebPolicyBoundCachePackage to disk - '{0}'",
                            genE.Message),
                        genE), true);
            }

            return false;
        }

        #endregion

        public const string PostParamKeyDisableCacheRetrieval = "bb_disable_cache";

        #region Retrieval methods

        #region Delegates

        /// <summary>
        /// Delegate used for constructing an object if it was not already added to the cache
        /// </summary>
        /// <param name="generateObjectParams"></param>
        /// <returns></returns>
        public delegate ICacheable ObjectNotInCacheDelegate(object[] generateObjectParams);

        #endregion

        private const string CacheIsEnabled_ContextKey = "dvnv03895tyskjdcvnsmn";

        public static bool CacheIsEnabled
        {
            get
            {
                var check = HttpContext.Current.Items[CacheIsEnabled_ContextKey] as bool?;
                if (!check.HasValue)
                {
                    check = !PostParams.GetBoolean(PostParamKeyDisableCacheRetrieval, false);
                    HttpContext.Current.Items[CacheIsEnabled_ContextKey] = check;
                }
                return check.Value;
            }
        }

        internal static WebPolicyBoundCachePackage RetrievePolicy(string cacheKey)
        {
            if (!CacheIsEnabled)
                return null;

            #region Validate

            if (string.IsNullOrEmpty(cacheKey))
                throw new ArgumentNullException("cacheKey");

            #endregion

            object returnOriginalObjectFromWithinPolicy = HttpRuntime.Cache[cacheKey];

            if (returnOriginalObjectFromWithinPolicy != null && returnOriginalObjectFromWithinPolicy is IPolicyBoundCachePackage)
                return (WebPolicyBoundCachePackage) returnOriginalObjectFromWithinPolicy;

            return null;
        }

        public static object RetrieveItemFromPolicy(string cacheKey)
        {
            if (!CacheIsEnabled)
                return null;

            #region Validate

            if (string.IsNullOrEmpty(cacheKey))
                throw new ArgumentNullException("cacheKey");

            #endregion

            //TODO: Update this method to use RetrievePolicy(cacheKey)

            object returnOriginalObjectFromWithinPolicy = HttpRuntime.Cache[cacheKey];

            if (returnOriginalObjectFromWithinPolicy != null && returnOriginalObjectFromWithinPolicy is IPolicyBoundCachePackage)
            {
                IPolicyBoundCachePackage checkPolicyContents = (PolicyBoundCachePackage) returnOriginalObjectFromWithinPolicy;
                if (checkPolicyContents.Item is CacheItemWrapper)
                    returnOriginalObjectFromWithinPolicy = ((CacheItemWrapper) checkPolicyContents.Item).Item;
                else
                {
                    // If the item is a disk cache, load it, otherwise return from RAM
                    if (checkPolicyContents.CacheStore == CacheStoreMechanism.PersistenceProvider)
                    {
                        // Item was cached to disk, therefore PolicyBoundCachePackage.Item
                        // will be a string path to its serialized version.
                        // We need to load that and replace PolicyBoundCachePackage.Item with it

                        //TODO: If we are loading from disk, what we need to do is add the
                        //deserialized version to the current context, so that repeated
                        //access to this cache item do not incurr repeated deserialization
                        //for a single page execution. We will also need to handle removing
                        //this from context if the cache is overwritten during a page lifecycle.
                        try
                        {
                            if (checkPolicyContents.Item != null &&
                                !string.IsNullOrEmpty(checkPolicyContents.Item.ToString()))
                            {
                                string loadFromPath = checkPolicyContents.Item.ToString();
                                IPolicyBoundCachePackage tryLoadPolicyFromDisk = Deserialize(loadFromPath);

                                if (tryLoadPolicyFromDisk != null && tryLoadPolicyFromDisk.Item != null)
                                    returnOriginalObjectFromWithinPolicy = tryLoadPolicyFromDisk.Item;
                                else
                                    returnOriginalObjectFromWithinPolicy = null;
                            }
                            else
                                returnOriginalObjectFromWithinPolicy = null;
                        }
                        catch
                        {
                            // Couldn't deserialize the file, so effectively invalidate the cache entry
                            returnOriginalObjectFromWithinPolicy = null;
                            //TODO: Delete the cache entry and the file
                        }
                    }
                    else
                    {
                        // Return the object from the Item property since it was not cached
                        // to disk
                        returnOriginalObjectFromWithinPolicy = checkPolicyContents.Item;
                    }
                }
            }

            return returnOriginalObjectFromWithinPolicy;
        }

        public static object Retrieve(Type objectType, CacheKeyParamsCollection instanceParams)
        {
            if (!CacheIsEnabled)
                return null;

            #region Validate

            if (objectType == null)
                throw new ArgumentNullException("objectType");
            if (instanceParams == null)
                throw new ArgumentNullException("instanceParams");

            #endregion

            return RetrieveItemFromPolicy(CacheKeyBuilder.GetKey(objectType, instanceParams));
        }

        /// <summary>
        /// Retrieves an object from cache, calling a method delegate if the cache does not contain an entry already. Saves the results of the callback to the cache for
        /// later retrieval. Use this method as a way of reducing code bulk in cache checking scenarios prior to populating your own objects.
        /// </summary>
        /// <returns></returns>
        public static object Retrieve(Type objectType, CacheKeyParamsCollection instanceParams,
                                      ObjectNotInCacheDelegate callbackIfCacheEmpty, object[] generateObjectParams)
        {
            if (!CacheIsEnabled)
                return null;

            object retValue = Retrieve(objectType, instanceParams);

            if (retValue == null)
            {
                retValue = callbackIfCacheEmpty(generateObjectParams);

                if (retValue != null)
                    Insert((ICacheable) retValue);
            }

            return retValue;
        }

        /// <summary>
        /// Loads a <see cref="WebPolicyBoundCachePackage"/> from disk.
        /// </summary>
        /// <param name="rootedFilePath"></param>
        /// <returns></returns>
        public static WebPolicyBoundCachePackage Deserialize(string rootedFilePath)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(rootedFilePath))
                throw new ArgumentNullException("rootedFilePath");

            #endregion

            return
                (WebPolicyBoundCachePackage)
                SerializationHelper.DeserializeCacheItemPolicy(rootedFilePath);
        }

        #endregion

        /// <summary>
        /// Method invoked as a <see cref="CacheItemRemovedCallback"/> delegate. If a cache item has been removed from the memory store,
        ///  we need to remove corresponding files from disk if they were
        /// persisted to the PersistenceProvider in the first place.
        /// </summary>
        protected static void OnCacheItemRemoved(string cacheKey, object cacheItem,
                                                 CacheItemRemovedReason internalReasonRemoved)
        {
            if (cacheItem is WebPolicyBoundCachePackage)
            {
                var itemPolicy = (WebPolicyBoundCachePackage) cacheItem;
                if (itemPolicy.CacheStore == CacheStoreMechanism.PersistenceProvider)
                {
                    // Only delete the file if we're not removing due to an update,
                    // as on the subsequent re-insert the file will get overwritten anyway
                    if (CacheRemovedReason != CacheRemovalReason.RemovedDueToUpdate)
                    {
                        // TODO: Mark the item for deletion from disk rather than deleting it here
                        if (itemPolicy.Item != null)
                        {
                            bool removeItem = true;
                            if (HostingEnvironment.ShutdownReason != ApplicationShutdownReason.None &&
                                itemPolicy.ShouldSurviveApplicationRestarts)
                            {
                                // The application is shutting, and the item is supposed to survive this, so don't delete
                                removeItem = false;
                            }

                            if (removeItem)
                            {
                                string serializationPath = itemPolicy.Item.ToString();
                                if (!string.IsNullOrEmpty(serializationPath))
                                {
                                    if (File.Exists(serializationPath))
                                    {
                                        try
                                        {
                                            IO.
                                                WaitUntilFileAvailable(serializationPath);
                                            Serialization.TryRemoveReadOnlyFlag(serializationPath);
                                            File.Delete(serializationPath);
                                        }
                                        catch
                                        {
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                switch (internalReasonRemoved)
                {
                    case CacheItemRemovedReason.Underused:
                        // If this machine has just removed the cache item to free memory,
                        // don't broadcast this to other machines.
                        // It's nothing to do with embarrassment, it's just that the other
                        // machines in the cluster will manage their own memory, and this
                        // saves on unnessary cache broadcast events.
                        break;
                    default:
                        try
                        {
                            // Only raise a BroadcastEvent if this delegate has been
                            // raised due to a normal cache removal situation.
                            bool doNotify = Configuration.Current.EnableCacheStateReplicationSink;
                            if (doNotify)
                            {
                                if (CacheRemovedReason == CacheRemovalReason.RemovedDueToUpdate)
                                    doNotify = false;
                            }

                            // Now check if the ReplicationSink is enabled, and if so, tell it to delete
                            // the notification file
                            if (doNotify)
                                SinkManager.Broadcast(BroadcastEventReason.ItemDeleted, itemPolicy);
                        }
                        finally
                        {
                            // Set the removal notification value back to normal
                            CacheRemovedReason = CacheRemovalReason.NormalRemoval;
                        }
                        break;
                }
            }
        }

        /// <summary>
        /// Reloads all items in the path returned by <see cref="GetPermanentDiskPersistenceRootedPath"/> into the current memory cache,
        /// whilst temporarily turning off ReplicationSink notification. Designed for use at application startup to reduce warmup times.
        /// </summary>
        /// <returns>The number of items that were successfully reloaded into cache.</returns>
        public static int ReloadAllPermanentCaches()
        {
            return ReloadAllPermanentCaches(false);
        }

        /// <summary>
        /// Reloads all items in the path returned by <see cref="GetPermanentDiskPersistenceRootedPath"/> into the current memory cache,
        /// whilst temporarily turning off ReplicationSink notification. Designed for use at application startup to reduce warmup times.
        /// </summary>
        /// <returns>The number of items that were successfully reloaded into cache.</returns>
        public static int ReloadAllPermanentCaches(bool throttleLoop)
        {
            string rootPath = GetPermanentDiskPersistenceRootedPath();
            int reloadedCount = 0;
            if (Directory.Exists(rootPath))
            {
                foreach (string file in Directory.GetFiles(rootPath))
                {
                    try
                    {
                        // Load the policy from disk
                        WebPolicyBoundCachePackage loadPolicy = Deserialize(file);

                        if (loadPolicy.AbsoluteExpiration > DateTime.Now)
                        {
                            // Insert into the cache following normal rules
                            ReInsertLoadedCacheItem(loadPolicy, file);
                            reloadedCount++;
                        }
                        else
                        {
                            // Item has expired since it was persisted, so try to delete it
                            try
                            {
                                // Delete file
                                File.Delete(file);
                            }
                            catch
                            {
                                /* Ignore error */
                            }
                        }
                    }
                    catch
                    {
                        // Failed to deserialize file, so try to delete it
                        try
                        {
                            // Delete file
                            File.Delete(file);
                        }
                        catch
                        {
                            /* Ignore error */
                        }
                    }

                    if (throttleLoop)
                        Thread.Sleep(200);
                }
            }
            return reloadedCount;
        }

        /// <summary>
        /// Attempts to delete all permanent cache files in the folder path returned by <see cref="GetPermanentDiskPersistenceRootedPath"/>
        /// </summary>
        public static void DeleteAllPermanentCaches()
        {
            string rootPath = GetPermanentDiskPersistenceRootedPath();
            if (Directory.Exists(rootPath))
            {
                foreach (string file in Directory.GetFiles(rootPath))
                {
                    try
                    {
                        // Delete the item from disk
                        File.Delete(file);
                    }
                    catch
                    {
                        /* Ignore error */
                    }
                }
            }
        }

        /// <summary>
        /// Gets the count of the files in the permanent cache store folder as specified by <see cref="GetPermanentDiskPersistenceRootedPath"/>.
        /// </summary>
        /// <returns></returns>
        public static int GetPermanentCacheItemsCount()
        {
            string rootPath = GetPermanentDiskPersistenceRootedPath();
            if (Directory.Exists(rootPath))
                return Directory.GetFiles(rootPath).Length;
            return 0;
        }

        #region Path generation methods

        /// <summary>
        /// Gets the rooted path of the folder for cache entries that will be persisted to disk.
        /// </summary>
        /// <returns></returns>
        public static string GetDiskPersistenceRootedPath()
        {
            return Path.StaticMapPath(Configuration.Current.CacheItemDiskPersistencePath);
        }

        /// <summary>
        /// Gets the rooted path of the folder for cache entries that will be permanently persisted to disk.
        /// </summary>
        /// <returns></returns>
        public static string GetPermanentDiskPersistenceRootedPath()
        {
            return Path.StaticMapPath(Configuration.Current.CacheItemPermanentDiskPersistencePath);
        }

        /// <summary>
        /// Gets a valid path for serializing a cache entry to disk.
        /// </summary>
        /// <returns></returns>
        /// <seealso cref="Web.GenerateSafePathForSerialization"/>
        /// <see cref="GetDiskPersistenceRootedPath"/>
        public static string GetSafePathForSerialization(string fileNameSeed)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(fileNameSeed))
                throw new ArgumentNullException("fileNameSeed");

            #endregion

            return Path.GenerateSafePathForSerialization(GetDiskPersistenceRootedPath(), fileNameSeed);
        }

        #endregion
    }
}